package com.neuronbit.businessflow.intellij.ui.projectView;

import com.intellij.ide.DeleteProvider;
import com.intellij.ide.projectView.ProjectViewNode;
import com.intellij.ide.projectView.TreeStructureProvider;
import com.intellij.ide.projectView.ViewSettings;
import com.intellij.ide.projectView.impl.nodes.BasePsiNode;
import com.intellij.ide.util.DeleteHandler;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassOwner;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.containers.ContainerUtil;
import com.neuronbit.businessflow.intellij.BpmFileType;
import com.neuronbit.businessflow.intellij.index.FlowClassIndex;
import org.jetbrains.annotations.NotNull;

import java.util.*;

/**
 * The type Flow tree structure provider.
 */
public class FlowTreeStructureProvider implements TreeStructureProvider {
    private final Project myProject;

    /**
     * Instantiates a new Flow tree structure provider.
     *
     * @param project the project
     */
    public FlowTreeStructureProvider(Project project) {
        myProject = project;
    }

    @Override
    public @NotNull Collection<AbstractTreeNode<?>> modify(@NotNull AbstractTreeNode<?> parent,
                                                           @NotNull Collection<AbstractTreeNode<?>> children,
                                                           ViewSettings settings) {
        if (parent.getValue() instanceof Flow) {
            return children;
        }

        // Optimization. Check if there are any forms at all.
        boolean formsFound = false;
        for (AbstractTreeNode<?> node : children) {
            if (node.getValue() instanceof PsiFile file) {
                if (file.getFileType() == BpmFileType.INSTANCE) {
                    formsFound = true;
                    break;
                }
            }
        }

        if (!formsFound) {
            return children;
        }

        Collection<AbstractTreeNode<?>> result = new LinkedHashSet<>(children);
        ProjectViewNode<?>[] copy = children.toArray(new ProjectViewNode[0]);
        for (ProjectViewNode<?> element : copy) {
            PsiClass psiClass = null;
            if (element.getValue() instanceof PsiClass) {
                psiClass = (PsiClass) element.getValue();
            } else if (element.getValue() instanceof PsiClassOwner) {
                final PsiClass[] psiClasses = ((PsiClassOwner) element.getValue()).getClasses();
                if (psiClasses.length == 1) {
                    psiClass = psiClasses[0];
                }
            }
            if (psiClass == null) continue;
            String qName = psiClass.getQualifiedName();
            if (qName == null) continue;
            List<PsiFile> forms = FlowClassIndex.findFormsBoundToClass(myProject, qName);
            Collection<BasePsiNode<? extends PsiElement>> formNodes = findFormsIn(children, forms);
            if (!formNodes.isEmpty()) {
                Collection<PsiFile> formFiles = convertToFiles(formNodes);
                Collection<BasePsiNode<? extends PsiElement>> subNodes = new ArrayList<>();
                subNodes.add((BasePsiNode<? extends PsiElement>) element);
                subNodes.addAll(formNodes);
                result.add(new FlowNode(myProject, new Flow(psiClass, formFiles), settings, subNodes));
                result.remove(element);
                result.removeAll(formNodes);
            }
        }
        return result;
    }

    @Override
    public Object getData(@NotNull Collection<? extends AbstractTreeNode<?>> selected, @NotNull String dataId) {
        if (Flow.DATA_KEY.is(dataId)) {
            List<Flow> result = new ArrayList<>();
            for (AbstractTreeNode<?> node : selected) {
                if (node instanceof FlowNode) {
                    result.add(((FlowNode)node).getValue());
                }
            }
            if (!result.isEmpty()) {
                return result.toArray(new Flow[0]);
            }
        }
        else if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
            for (AbstractTreeNode<?> node : selected) {
                if (node instanceof FlowNode) {
                    return new MyDeleteProvider(selected);
                }
            }
        }
        return null;
    }

    private static Collection<PsiFile> convertToFiles(Collection<? extends BasePsiNode<? extends PsiElement>> formNodes) {
        List<PsiFile> psiFiles = new ArrayList<>();
        for (AbstractTreeNode<?> treeNode : formNodes) {
            psiFiles.add((PsiFile) treeNode.getValue());
        }
        return psiFiles;
    }

    private static Collection<BasePsiNode<? extends PsiElement>> findFormsIn(Collection<? extends AbstractTreeNode<?>> children, List<? extends PsiFile> forms) {
        if (children.isEmpty() || forms.isEmpty()) return Collections.emptyList();
        List<BasePsiNode<? extends PsiElement>> result = new ArrayList<>();
        Set<PsiFile> psiFiles = new HashSet<>(forms);
        for (final AbstractTreeNode<?> child : children) {
            if (child instanceof BasePsiNode<? extends PsiElement> treeNode) {
                if (psiFiles.contains(treeNode.getValue())) {
                    result.add(treeNode);
                }
            }
        }
        return result;
    }

    private static final class MyDeleteProvider implements DeleteProvider {
        private final AbstractTreeNode<?>[] myNodes;

        /**
         * Instantiates a new My delete provider.
         *
         * @param nodes the nodes
         */
        MyDeleteProvider(Collection<? extends AbstractTreeNode<?>> nodes) {
            myNodes = nodes.toArray(AbstractTreeNode[]::new);
        }

        @Override
        public @NotNull ActionUpdateThread getActionUpdateThread() {
            return ActionUpdateThread.BGT;
        }

        @Override
        public void deleteElement(@NotNull DataContext dataContext) {
            Project project = CommonDataKeys.PROJECT.getData(dataContext);
            PsiElement[] elements = collectFormPsiElements(myNodes);
            DeleteHandler.deletePsiElement(elements, project);
        }

        @Override
        public boolean canDeleteElement(@NotNull DataContext dataContext) {
            PsiElement[] elements = collectFormPsiElements(myNodes);
            return DeleteHandler.shouldEnableDeleteAction(elements);
        }

        private static PsiElement[] collectFormPsiElements(AbstractTreeNode<?>[] selected) {
            Set<PsiElement> result = new HashSet<>();
            for(AbstractTreeNode<?> node: selected) {
                if (node.getValue() instanceof Flow flow) {
                    result.add(flow.getClassToBind());
                    ContainerUtil.addAll(result, flow.getFormFiles());
                }
                else if (node.getValue() instanceof PsiElement) {
                    result.add((PsiElement) node.getValue());
                }
            }
            return PsiUtilCore.toPsiElementArray(result);
        }
    }
}
